import json
import logging
import threading
import time
from http import HTTPStatus
from urllib.error import HTTPError

from flask import Flask

from env_config import RUN_ENV, public_key_decrypt
from nacos import NacosClient, DEFAULT_GROUP_NAME, NacosException


class NacosHeartbeatInfo:
    def __init__(self,
                 service_name,
                 ip,
                 port,
                 cluster_name=None,
                 weight=1.0,
                 metadata=None,
                 ephemeral=False,
                 group_name=DEFAULT_GROUP_NAME,
                 heartbeat_interval=5):
        self.service_name = service_name
        self.ip = ip
        self.port = port
        self.cluster_name = cluster_name
        self.weight = weight
        self.metadata = metadata
        self.ephemeral = ephemeral
        self.group_name = group_name
        self.heartbeat_interval = heartbeat_interval


class NacosHeartbeatTask(threading.Thread):
    """
    Nacos Heart Task Class
    """

    def __init__(self, beat_info: NacosHeartbeatInfo, client: NacosClient):
        self.logger = logging.getLogger('nacos.client')
        super().__init__()
        self.beat_info = beat_info
        self.client = client
        self.daemon = True
        self.stopped = False
        self.access_token = None
        #
        self._init_client()

    def _init_client(self):
        setattr(self.client, 'token', '')
        setattr(self.client, 'token_ttl', '')
        setattr(self.client, 'token_expire_time', '')

    def run(self):
        self.logger.info("[auto-beat-task] beat task start, ip:%s, port:%s, service_name:%s, group_name:%s" % (
            self.beat_info.ip, self.beat_info.port, self.beat_info.service_name, self.beat_info.group_name))
        while not self.stopped:
            try:
                self.access_token = self.get_access_token()
                self.send_heartbeat(self.beat_info.service_name,
                                           self.beat_info.ip,
                                           self.beat_info.port,
                                           self.beat_info.cluster_name,
                                           self.beat_info.weight,
                                           self.beat_info.metadata,
                                           self.beat_info.ephemeral,
                                           self.beat_info.group_name)
                time.sleep(self.beat_info.heartbeat_interval)
            except Exception as ex:
                self.logger.error("[auto-beat-task] beat task error: %s" % ex)
                time.sleep(self.beat_info.heartbeat_interval)
        self.logger.info("[auto-beat-task] beat task stopped, ip:%s, port:%s, service_name:%s, group_name:%s" % (
            self.beat_info.ip, self.beat_info.port, self.beat_info.service_name, self.beat_info.group_name))

    def stop(self):
        self.stopped = True

    def get_access_token(self, force_refresh=False):
        current_time = time.time()
        if self.client.token and not force_refresh and self.client.token_expire_time > current_time:
            return self.client.token

        params = {
            "username": self.client.username,
            "password": self.client.password
        }
        try:
            resp = self.client._do_sync_req("/nacos/v1/auth/login", None, params, None, self.client.default_timeout,
                                            "POST")
            c = resp.read()
            response_data = json.loads(c.decode("UTF-8"))
            self.client.token = response_data.get('accessToken')
            self.client.token_ttl = response_data.get('tokenTtl', 18000)  # 默认使用返回值，无返回则使用18000秒
            self.client.token_expire_time = current_time + self.client.token_ttl - 10  # 更新 Token 的过期时间
            self.logger.info(
                "[get_access_token] AccessToken: %s, TTL: %s，force_refresh：%s", self.client.token,
                self.client.token_ttl, force_refresh)
        except Exception as e:
            self.logger.error("[get-access-token] exception %s occur", str(e))
            raise
        return self.client.token

    def send_heartbeat(self, service_name, ip, port, cluster_name=None, weight=1.0, metadata=None, ephemeral=True,
                       group_name=DEFAULT_GROUP_NAME):
        self.logger.info("[send-heartbeat] ip:%s, port:%s, service_name:%s, namespace:%s" % (ip, port, service_name,
                                                                                             self.client.namespace))
        beat_data = {
            "serviceName": service_name,
            "ip": ip,
            "port": port,
            "weight": weight,
            "ephemeral": ephemeral
        }

        if cluster_name is not None:
            beat_data["cluster"] = cluster_name

        if metadata is not None:
            if isinstance(metadata, str):
                beat_data["metadata"] = json.loads(metadata)
            else:
                beat_data["metadata"] = metadata

        params = {
            "serviceName": service_name,
            "beat": json.dumps(beat_data),
            "groupName": group_name,
            'accessToken': self.access_token
        }

        if self.client.namespace:
            params["namespaceId"] = self.client.namespace

        try:
            resp = self.client._do_sync_req("/nacos/v1/ns/instance/beat", None, params, None,
                                            self.client.default_timeout, "PUT")
            c = resp.read()
            self.logger.info("[send-heartbeat] ip:%s, port:%s, service_name:%s, namespace:%s, server response:%s" %
                             (ip, port, service_name, self.client.namespace, c))
            return json.loads(c.decode("UTF-8"))
        except HTTPError as e:
            if e.code == HTTPStatus.FORBIDDEN:
                raise NacosException("Insufficient privilege.")
            else:
                raise NacosException("Request Error, code is %s" % e.code)
        except Exception as e:
            self.logger.exception("[send-heartbeat] exception %s occur" % str(e))
            raise


class NacosService(object):
    """
    Nacos Service Class
    """
    _HOST_INFO = None

    NACOS_ENABLED = False

    # Nacos
    NACOS_SERVER_ADDRESSES = None
    NACOS_NAMESPACE = None
    NACOS_AK = None
    NACOS_SK = None
    # 密码加密
    NACOS_DECRYPT_KEY = None
    NACOS_ENCRYPT_SK = None

    # Service
    NACOS_SERVICE_NAME = None
    NACOS_SERVICE_IP = None
    NACOS_SERVICE_PORT = None
    NACOS_SERVICE_HOSTNAME = None
    NACOS_SERVICE_GROUP_NAME = None
    NACOS_SERVICE_HEARTBEAT_INTERVAL = None
    NACOS_SERVICE_EPHEMERAL = None

    def __init__(self, app: Flask):
        app.config.from_prefixed_env(prefix='X')
        if not app.config.get('NACOS_ENABLED'):
            app.logger.warning('[NACOS] nacos not enabled')
            return
        app.logger.info('[NACOS] nacos enabled, try start...')
        self.app = app
        self._init()

    def _decrypt_key(self):
        """
        解密
        """
        if self.NACOS_DECRYPT_KEY and self.NACOS_ENCRYPT_SK:
            data = public_key_decrypt(self.NACOS_DECRYPT_KEY, self.NACOS_ENCRYPT_SK)
            self.app.logger.info('[Nacos] Nacos decrypt key success')
            self.NACOS_SK = data

    def _init(self):
        """
        初始化
        """
        # 设置初始化值
        for key in NacosService.__dict__.keys():
            if not key.startswith('NACOS'):
                continue
            setattr(self, key, self.app.config.get(key))
        # 默认分组
        if not self.NACOS_SERVICE_GROUP_NAME:
            self.NACOS_SERVICE_GROUP_NAME = DEFAULT_GROUP_NAME,
        # 解密
        self._decrypt_key()
        # Nacos服务注册
        try:
            self.nacos_client = NacosClient(
                self.NACOS_SERVER_ADDRESSES,
                namespace=self.NACOS_NAMESPACE,
                username=self.NACOS_AK,
                password=self.NACOS_SK)
            self.register_service(self.NACOS_SERVICE_NAME)
            self.app.logger.info('[NACOS] nacos service registed success, service name: %s, ip: %s, port: %s',
                                 self.NACOS_SERVICE_NAME, self.NACOS_SERVICE_IP, self.NACOS_SERVICE_PORT)
        except Exception as ex:
            self.app.logger.error('[NACOS] nacos start error: %s', ex)
        # 测试环境
        try:
            if RUN_ENV.is_test():
                self.app.logger.info('[NACOS] (TEST) nacos service try register')
                for srv in ('pc', 'pm', 'tw', 'yc'):
                    self.register_service(self.NACOS_SERVICE_NAME + '-' + srv)
                self.app.logger.info('[NACOS] (TEST) nacos service registed success')
        except Exception as ex:
            self.app.logger.error('[NACOS] (TEST) nacos start error: %s', ex)

    def host_info(self):
        if self._HOST_INFO:
            return self._HOST_INFO
        if self.NACOS_ENABLED:
            self._HOST_INFO = f'({self.NACOS_SERVICE_HOSTNAME}){self.NACOS_SERVICE_IP}:{self.NACOS_SERVICE_PORT}'
        else:
            self._HOST_INFO = 'invalid'
        return self._HOST_INFO

    def register_service(self, service_name):
        self.nacos_client.add_naming_instance(
            service_name=service_name,
            ip=self.NACOS_SERVICE_IP,
            port=self.NACOS_SERVICE_PORT,
            ephemeral=self.NACOS_SERVICE_EPHEMERAL,
            group_name=self.NACOS_SERVICE_GROUP_NAME,
        )
        # 仅在 ephemeral 模式下才需要维持心跳
        if self.NACOS_SERVICE_EPHEMERAL:
            # 维持心跳
            nacos_beat_info = NacosHeartbeatInfo(
                service_name=service_name,
                ip=self.NACOS_SERVICE_IP,
                port=self.NACOS_SERVICE_PORT,
                ephemeral=self.NACOS_SERVICE_EPHEMERAL,
                group_name=self.NACOS_SERVICE_GROUP_NAME,
                heartbeat_interval=self.NACOS_SERVICE_HEARTBEAT_INTERVAL,
            )
            nacos_beat_task = NacosHeartbeatTask(nacos_beat_info, self.nacos_client)
            nacos_beat_task.start()

    def unregister_service(self):
        self.nacos_client.remove_naming_instance(
            service_name=self.NACOS_SERVICE_NAME,
            ip=self.NACOS_SERVICE_IP,
            port=self.NACOS_SERVICE_PORT,
        )
